package cool.houge.pig.infra.web;

import am.ik.yavi.core.ConstraintViolationsException;
import com.fasterxml.jackson.core.JsonParseException;
import com.google.common.collect.ImmutableMap;
import cool.houge.pig.BizCodeException;
import cool.houge.pig.service.auth.AuthTokenException;
import cool.houge.pig.service.webhook.WebhookException;
import cool.houge.pig.util.MissingQueryParameterException;
import cool.houge.pig.util.WrongTypeQueryParameterException;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import reactor.core.publisher.Mono;

/** @author KK (kzou227@qq.com) */
@Component
public class WebErrorHandler {

  private static final Logger log = LoggerFactory.getLogger(WebErrorHandler.class);

  /**
   * @param request
   * @param t
   * @return
   */
  public Mono<ServerResponse> handle(Throwable t, ServerRequest request) {
    log.debug("HTTP请求错误: {} {}", request.methodName(), request.requestPath(), t);

    // 打印调试日志并输出异常堆栈
    boolean debugLog = request.queryParam("debug").isPresent();
    // 打印错误日志
    Map<String, Object> error = new LinkedHashMap<>();
    if (debugLog) {
      error.put("stacktrace", getStackTrace(t));
    }

    // ===================================== Spring Web 错误 =====================================
    if (t instanceof ServerWebInputException) {
      // FIXME: InvalidFormatException
      var ex = (ServerWebInputException) t;
      if (ex.getRootCause() instanceof JsonParseException) {
        error.put("title", "非法的 JSON 请求内容");
        error.put("message", ex.getRootCause().getMessage());
      } else {
        error.put("title", "请求数据解析错误");
        error.put("message", ex.getMessage());
      }
      return ServerResponse.status(ex.getStatus()).bodyValue(error);
    }
    if (t instanceof UnsupportedMediaTypeStatusException) {
      var ex = (UnsupportedMediaTypeStatusException) t;
      error.put("title", "不支持的 content-type");
      error.put("message", ex.getMessage());
      return ServerResponse.status(ex.getStatus()).bodyValue(error);
    }

    // ===================================== 自定义错误 =====================================
    if (t instanceof ConstraintViolationsException) {
      var ex = (ConstraintViolationsException) t;
      error.put("title", "请求输入校验失败");
      var details =
          ex.violations().stream()
              .map(
                  violation ->
                      ImmutableMap.builder()
                          .put("message", violation.message())
                          .put("field", violation.name())
                          .build()
                  //
                  )
              .collect(Collectors.toList());
      error.put("errors", details);
      return ServerResponse.badRequest().bodyValue(error);
    }
    if (t instanceof MissingQueryParameterException) {
      var ex = (MissingQueryParameterException) t;
      error.put("title", "缺少查询参数");
      error.put("query_parameter_name", ex.getParameterName());
      return ServerResponse.badRequest().bodyValue(error);
    }
    if (t instanceof WrongTypeQueryParameterException) {
      var ex = (WrongTypeQueryParameterException) t;
      error.put("title", "查询参数数据类型不正确");
      error.put("query_parameter_name", ex.getParameterName());
      error.put("query_parameter_value", ex.getParameterValue());
      return ServerResponse.badRequest().bodyValue(error);
    }
    if (t instanceof BizCodeException) {
      var ex = (BizCodeException) t;
      var bizError = ex.getBizError();
      error.put("code", bizError.code());
      error.put("title", bizError.message());
      if (ex.getRawMessage() != null) {
        error.put("raw_message", ex.getRawMessage());
      }
      return ServerResponse.status(bizError.httpStatus()).bodyValue(error);
    }
    if (t instanceof WebhookException) {
      var ex = (WebhookException) t;
      var problem =
          Problem.builder()
              .code(800)
              .title("远程服务调用失败")
              .detail(ex.getMessage())
              .path(request.path())
              .method(request.methodName())
              .parameter("webhook_url", ex.getUrl())
              .build();
      return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue(problem);
    }

    // 认证异常
    if (t instanceof AuthTokenException) {
      var ex = (AuthTokenException) t;
      var problem =
          Problem.builder()
              .parameter("error", ex.getError().getError())
              .parameter("error_description", ex.getMessage())
              .build();
      return ServerResponse.status(ex.getError().getHttpStatus()).bodyValue(problem);
    }

    // ===================================== 数据库错误 =====================================
    if (t instanceof DataIntegrityViolationException) {
      log.error("数据库约束错误 requestPath={}", request.requestPath(), t);
      if (t.getCause() instanceof R2dbcDataIntegrityViolationException) {
        var ex = (R2dbcDataIntegrityViolationException) t.getCause();
        if ("23000".equals(ex.getSqlState())) {
          error.put("title", "唯一约束冲突");
        } else {
          error.put("title", "数据库约束错误");
        }
        error.put("message", ex.getMessage());
        error.put("sql_state", ex.getSqlState());
        return ServerResponse.status(500).bodyValue(error);
      }
      error.put("title", "数据库约束错误");
      error.put("message", t.getMessage());
      return ServerResponse.status(500).bodyValue(error);
    }

    log.error("请求出现未处理的异常 requestPath={}", request.requestPath(), t);
    error.put("title", "服务器发生未知错误");
    error.put("message", t.getMessage());
    return ServerResponse.status(500).bodyValue(error);
  }

  private Stream<String> getStackTrace(Throwable t) {
    StringWriter sw = new StringWriter();
    t.printStackTrace(new PrintWriter(sw));
    return sw.toString()
        .lines()
        .map(
            s -> {
              String rs = s;
              while (rs.indexOf("\t") != -1) {
                rs = rs.replaceFirst("\t", "  ");
              }
              return rs;
            });
  }
}
